"""
Exception handling and error management for document analysis module.

Provides custom exception classes, error handling utilities, and
centralized error reporting mechanisms.
"""

import logging
import traceback
from datetime import datetime
from typing import Dict, Any, Optional, List, Union, Callable
from functools import wraps
from pathlib import Path
import sys


# Base Exception Classes

class DocumentAnalysisError(Exception):
    """Base exception for document analysis module."""

    def __init__(self, message: str, error_code: str = None, context: Dict[str, Any] = None):
        """Initialize exception with enhanced context.

        Args:
            message: Error message
            error_code: Optional error code for categorization
            context: Additional context information
        """
        super().__init__(message)
        self.message = message
        self.error_code = error_code or self.__class__.__name__
        self.context = context or {}
        self.timestamp = datetime.now()
        self.traceback_str = traceback.format_exc()

    def to_dict(self) -> Dict[str, Any]:
        """Convert exception to dictionary for logging/serialization.

        Returns:
            Dictionary representation of exception
        """
        return {
            "error_type": self.__class__.__name__,
            "error_code": self.error_code,
            "message": self.message,
            "context": self.context,
            "timestamp": self.timestamp.isoformat(),
            "traceback": self.traceback_str
        }


# Document Processing Exceptions

class DocumentProcessingError(DocumentAnalysisError):
    """Raised when document processing fails."""

    def __init__(self, message: str, file_path: str = None, **context):
        context.setdefault("file_path", file_path)
        super().__init__(message, "DOC_PROCESSING_ERROR", context)


class DocumentParsingError(DocumentProcessingError):
    """Raised when document parsing fails."""

    def __init__(self, message: str, file_path: str = None, line_number: int = None, **context):
        if line_number is not None:
            context["line_number"] = line_number
        super().__init__(message, file_path, **context)


class UnsupportedDocumentTypeError(DocumentProcessingError):
    """Raised when document type is not supported."""

    def __init__(self, file_path: str, document_type: str, supported_types: List[str]):
        message = f"Unsupported document type: {document_type}"
        super().__init__(
            message,
            file_path,
            document_type=document_type,
            supported_types=supported_types
        )


class DocumentSizeError(DocumentProcessingError):
    """Raised when document exceeds size limits."""

    def __init__(self, file_path: str, file_size: int, max_size: int):
        message = f"Document size ({file_size} bytes) exceeds maximum allowed size ({max_size} bytes)"
        super().__init__(
            message,
            file_path,
            file_size=file_size,
            max_size=max_size
        )


# Analysis Exceptions

class AnalysisError(DocumentAnalysisError):
    """Raised when document analysis fails."""

    def __init__(self, message: str, document_id: str = None, **context):
        context.setdefault("document_id", document_id)
        super().__init__(message, "ANALYSIS_ERROR", context)


class ClarityEvaluationError(AnalysisError):
    """Raised when clarity evaluation fails."""

    def __init__(self, message: str, document_id: str = None, evaluation_step: str = None, **context):
        if evaluation_step is not None:
            context["evaluation_step"] = evaluation_step
        super().__init__(message, document_id, **context)


class MetricsCalculationError(AnalysisError):
    """Raised when metrics calculation fails."""

    def __init__(self, message: str, metric_type: str = None, **context):
        if metric_type is not None:
            context["metric_type"] = metric_type
        super().__init__(message, **context)


class AmbiguityDetectionError(AnalysisError):
    """Raised when ambiguity detection fails."""

    def __init__(self, message: str, detection_method: str = None, **context):
        if detection_method is not None:
            context["detection_method"] = detection_method
        super().__init__(message, **context)


# Storage Exceptions

class StorageError(DocumentAnalysisError):
    """Raised when storage operations fail."""

    def __init__(self, message: str, operation: str = None, **context):
        if operation is not None:
            context["operation"] = operation
        super().__init__(message, "STORAGE_ERROR", context)


class DatabaseError(StorageError):
    """Raised when database operations fail."""

    def __init__(self, message: str, query: str = None, table: str = None, **context):
        if query is not None:
            context["query"] = query
        if table is not None:
            context["table"] = table
        super().__init__(message, "database", **context)


class FileStorageError(StorageError):
    """Raised when file storage operations fail."""

    def __init__(self, message: str, file_path: str = None, **context):
        if file_path is not None:
            context["file_path"] = str(file_path)
        super().__init__(message, "file_storage", **context)


# Configuration Exceptions

class ConfigurationError(DocumentAnalysisError):
    """Raised when configuration is invalid."""

    def __init__(self, message: str, config_key: str = None, config_value: Any = None, **context):
        if config_key is not None:
            context["config_key"] = config_key
        if config_value is not None:
            context["config_value"] = config_value
        super().__init__(message, "CONFIGURATION_ERROR", context)


class MissingConfigurationError(ConfigurationError):
    """Raised when required configuration is missing."""

    def __init__(self, config_key: str, config_section: str = None):
        message = f"Missing required configuration: {config_key}"
        if config_section:
            message += f" (section: {config_section})"
        super().__init__(message, config_key, config_section=config_section)


# Performance and Resource Exceptions

class PerformanceError(DocumentAnalysisError):
    """Raised when performance limits are exceeded."""

    def __init__(self, message: str, metric: str = None, threshold: Any = None, actual_value: Any = None, **context):
        if metric is not None:
            context["metric"] = metric
        if threshold is not None:
            context["threshold"] = threshold
        if actual_value is not None:
            context["actual_value"] = actual_value
        super().__init__(message, "PERFORMANCE_ERROR", context)


class TimeoutError(PerformanceError):
    """Raised when operation times out."""

    def __init__(self, operation: str, timeout_seconds: int, actual_duration: float):
        message = f"Operation '{operation}' timed out after {timeout_seconds}s (actual: {actual_duration:.2f}s)"
        super().__init__(
            message,
            "timeout",
            threshold=timeout_seconds,
            actual_value=actual_duration,
            operation=operation
        )


class MemoryError(PerformanceError):
    """Raised when memory usage exceeds limits."""

    def __init__(self, operation: str, memory_mb: int, limit_mb: int):
        message = f"Memory usage ({memory_mb}MB) exceeds limit ({limit_mb}MB) during {operation}"
        super().__init__(
            message,
            "memory",
            threshold=limit_mb,
            actual_value=memory_mb,
            operation=operation
        )


# Error Handler and Utilities

class ErrorHandler:
    """Centralized error handling and reporting."""

    def __init__(self, logger: logging.Logger = None):
        """Initialize error handler.

        Args:
            logger: Logger instance for error reporting
        """
        self.logger = logger or logging.getLogger(__name__)
        self.error_counts = {}
        self.error_history = []

    def handle_exception(self, exception: Exception, context: Dict[str, Any] = None) -> Dict[str, Any]:
        """Handle and log exception.

        Args:
            exception: Exception to handle
            context: Additional context

        Returns:
            Error information dictionary
        """
        error_info = self._create_error_info(exception, context)
        self._log_error(error_info)
        self._track_error(error_info)
        return error_info

    def _create_error_info(self, exception: Exception, context: Dict[str, Any] = None) -> Dict[str, Any]:
        """Create standardized error information.

        Args:
            exception: Exception to process
            context: Additional context

        Returns:
            Error information dictionary
        """
        if isinstance(exception, DocumentAnalysisError):
            error_info = exception.to_dict()
        else:
            error_info = {
                "error_type": exception.__class__.__name__,
                "error_code": "UNKNOWN_ERROR",
                "message": str(exception),
                "context": context or {},
                "timestamp": datetime.now().isoformat(),
                "traceback": traceback.format_exc()
            }

        # Add system context
        error_info["system_context"] = {
            "python_version": sys.version,
            "platform": sys.platform
        }

        return error_info

    def _log_error(self, error_info: Dict[str, Any]):
        """Log error with appropriate level.

        Args:
            error_info: Error information to log
        """
        error_type = error_info["error_type"]
        message = error_info["message"]

        # Determine log level based on error type
        if any(keyword in error_type.lower() for keyword in ["configuration", "missing"]):
            level = logging.CRITICAL
        elif any(keyword in error_type.lower() for keyword in ["storage", "database"]):
            level = logging.ERROR
        elif any(keyword in error_type.lower() for keyword in ["performance", "timeout", "memory"]):
            level = logging.WARNING
        else:
            level = logging.ERROR

        self.logger.log(
            level,
            f"[{error_info['error_code']}] {message}",
            extra={
                "error_info": error_info,
                "error_type": error_type,
                "error_code": error_info["error_code"]
            }
        )

    def _track_error(self, error_info: Dict[str, Any]):
        """Track error statistics.

        Args:
            error_info: Error information to track
        """
        error_code = error_info["error_code"]
        self.error_counts[error_code] = self.error_counts.get(error_code, 0) + 1

        # Keep limited history
        self.error_history.append(error_info)
        if len(self.error_history) > 100:  # Keep last 100 errors
            self.error_history.pop(0)

    def get_error_statistics(self) -> Dict[str, Any]:
        """Get error handling statistics.

        Returns:
            Error statistics dictionary
        """
        return {
            "error_counts": self.error_counts.copy(),
            "total_errors": sum(self.error_counts.values()),
            "recent_errors": len(self.error_history),
            "most_common_errors": sorted(
                self.error_counts.items(),
                key=lambda x: x[1],
                reverse=True
            )[:10]
        }

    def clear_statistics(self):
        """Clear error statistics."""
        self.error_counts.clear()
        self.error_history.clear()


# Decorators for Error Handling

def handle_errors(
    error_handler: ErrorHandler = None,
    reraise: bool = True,
    default_return: Any = None,
    log_context: Dict[str, Any] = None
):
    """Decorator for automatic error handling.

    Args:
        error_handler: ErrorHandler instance to use
        reraise: Whether to reraise exception after handling
        default_return: Default return value on error (if not reraising)
        log_context: Additional context to log with errors
    """
    def decorator(func: Callable):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                # Create context
                context = {
                    "function": func.__name__,
                    "module": func.__module__,
                    "args_count": len(args),
                    "kwargs_keys": list(kwargs.keys())
                }
                if log_context:
                    context.update(log_context)

                # Handle error
                handler = error_handler or ErrorHandler()
                error_info = handler.handle_exception(e, context)

                if reraise:
                    raise
                else:
                    return default_return

        return wrapper
    return decorator


def handle_async_errors(
    error_handler: ErrorHandler = None,
    reraise: bool = True,
    default_return: Any = None,
    log_context: Dict[str, Any] = None
):
    """Decorator for automatic async error handling.

    Args:
        error_handler: ErrorHandler instance to use
        reraise: Whether to reraise exception after handling
        default_return: Default return value on error (if not reraising)
        log_context: Additional context to log with errors
    """
    def decorator(func: Callable):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            try:
                return await func(*args, **kwargs)
            except Exception as e:
                # Create context
                context = {
                    "function": func.__name__,
                    "module": func.__module__,
                    "async_function": True,
                    "args_count": len(args),
                    "kwargs_keys": list(kwargs.keys())
                }
                if log_context:
                    context.update(log_context)

                # Handle error
                handler = error_handler or ErrorHandler()
                error_info = handler.handle_exception(e, context)

                if reraise:
                    raise
                else:
                    return default_return

        return wrapper
    return decorator


# Utility Functions

def create_error_response(error_info: Dict[str, Any]) -> Dict[str, Any]:
    """Create standardized error response for API endpoints.

    Args:
        error_info: Error information from ErrorHandler

    Returns:
        Standardized error response
    """
    return {
        "success": False,
        "error": {
            "code": error_info["error_code"],
            "type": error_info["error_type"],
            "message": error_info["message"],
            "timestamp": error_info["timestamp"]
        },
        "context": error_info.get("context", {})
    }


def validate_file_path(file_path: Union[str, Path], allowed_extensions: List[str] = None) -> Path:
    """Validate file path and raise appropriate exceptions.

    Args:
        file_path: Path to validate
        allowed_extensions: List of allowed file extensions

    Returns:
        Validated Path object

    Raises:
        DocumentProcessingError: If file doesn't exist or invalid
        UnsupportedDocumentTypeError: If extension not allowed
    """
    path = Path(file_path)

    if not path.exists():
        raise DocumentProcessingError(f"File does not exist: {file_path}", str(path))

    if not path.is_file():
        raise DocumentProcessingError(f"Path is not a file: {file_path}", str(path))

    if allowed_extensions:
        if path.suffix.lower() not in [ext.lower() for ext in allowed_extensions]:
            raise UnsupportedDocumentTypeError(
                str(path),
                path.suffix,
                allowed_extensions
            )

    return path


def check_performance_limits(
    operation: str,
    memory_mb: int = None,
    memory_limit_mb: int = None,
    duration_seconds: float = None,
    timeout_seconds: int = None
):
    """Check performance limits and raise exceptions if exceeded.

    Args:
        operation: Operation name for context
        memory_mb: Current memory usage in MB
        memory_limit_mb: Memory limit in MB
        duration_seconds: Operation duration in seconds
        timeout_seconds: Timeout limit in seconds

    Raises:
        MemoryError: If memory limit exceeded
        TimeoutError: If timeout exceeded
    """
    if memory_mb and memory_limit_mb and memory_mb > memory_limit_mb:
        raise MemoryError(operation, memory_mb, memory_limit_mb)

    if duration_seconds and timeout_seconds and duration_seconds > timeout_seconds:
        raise TimeoutError(operation, timeout_seconds, duration_seconds)


# Global error handler instance
_global_error_handler: Optional[ErrorHandler] = None


def get_error_handler() -> ErrorHandler:
    """Get global error handler instance.

    Returns:
        ErrorHandler instance
    """
    global _global_error_handler
    if _global_error_handler is None:
        _global_error_handler = ErrorHandler()
    return _global_error_handler


def set_error_handler(error_handler: ErrorHandler):
    """Set global error handler instance.

    Args:
        error_handler: ErrorHandler instance to set
    """
    global _global_error_handler
    _global_error_handler = error_handler